TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import type { Metadata } from "next";2import Link from "next/link";3import { notFound } from "next/navigation";4import { ArrowRight, Paperclip, Swords } from "lucide-react";5import { getPublicArenaShare } from "@/lib/arena/service";6import type { ArenaShareSnapshot } from "@/lib/arena/export";7import { Logo } from "@/components/brand/logo";8import { ProviderIcon } from "@/components/brand/provider-icon";9import { Badge } from "@/components/ui/badge";10import { Button } from "@/components/ui/button";11import { SimpleMarkdown } from "@/components/markdown/simple-markdown";12import { BlindNotice, SharedArenaView } from "@/components/arena/shared-arena-view";1314export const dynamic = "force-dynamic";1516function isRecord(v: unknown): v is Record<string, unknown> {17 return typeof v === "object" && v !== null;18}1920/** Defensive parse of the frozen snapshot written by `shareArenaSession`. */21function parseSnapshot(raw: unknown): ArenaShareSnapshot | null {22 if (!isRecord(raw) || typeof raw.prompt !== "string" || !Array.isArray(raw.responses)) return null;23 const responses = raw.responses.filter(isRecord).map((r, i) => ({24 id: typeof r.id === "string" ? r.id : `r${i}`,25 modelKey: typeof r.modelKey === "string" ? r.modelKey : "",26 provider: typeof r.provider === "string" ? r.provider : (typeof r.modelKey === "string" ? r.modelKey.split("/")[0] : ""),27 displayName: typeof r.displayName === "string" ? r.displayName : typeof r.modelKey === "string" ? r.modelKey.split("/").slice(1).join("/") : "Model",28 status: typeof r.status === "string" ? r.status : "complete",29 content: typeof r.content === "string" ? r.content : "",30 reasoning: typeof r.reasoning === "string" ? r.reasoning : null,31 error: isRecord(r.error) && typeof r.error.message === "string" ? { code: String(r.error.code ?? "ERROR"), message: r.error.message } : null,32 ttftMs: typeof r.ttftMs === "number" ? r.ttftMs : null,33 latencyMs: typeof r.latencyMs === "number" ? r.latencyMs : null,34 costUsd: typeof r.costUsd === "number" ? r.costUsd : null,35 usage: isRecord(r.usage) ? (r.usage as ArenaShareSnapshot["responses"][number]["usage"]) : null,36 criteriaWon: Array.isArray(r.criteriaWon) ? r.criteriaWon.filter((c): c is string => typeof c === "string") : [],37 }));38 const models = Array.isArray(raw.models) ? raw.models.filter(isRecord).map((m) => ({ key: String(m.key ?? ""), provider: String(m.provider ?? ""), displayName: String(m.displayName ?? m.key ?? "") })) : responses.map((r) => ({ key: r.modelKey, provider: r.provider, displayName: r.displayName }));39 const votes = Array.isArray(raw.votes) ? raw.votes.filter(isRecord).map((v) => ({ criterion: String(v.criterion ?? ""), label: String(v.label ?? v.criterion ?? ""), modelKey: String(v.modelKey ?? ""), responseId: String(v.responseId ?? "") })) : [];40 const w = isRecord(raw.winner) ? raw.winner : null;41 const winner =42 w && typeof w.modelKey === "string"43 ? {44 modelKey: w.modelKey,45 responseId: String(w.responseId ?? ""),46 criteriaWon: Array.isArray(w.criteriaWon) ? w.criteriaWon.filter((c): c is string => typeof c === "string") : [],47 tieBreak: w.tieBreak === "fastest" ? ("fastest" as const) : w.tieBreak === "order" ? ("order" as const) : null,48 deltas: isRecord(w.deltas) ? { costUsd: num(w.deltas.costUsd), ttftMs: num(w.deltas.ttftMs), latencyMs: num(w.deltas.latencyMs), outputTokens: num(w.deltas.outputTokens), others: num(w.deltas.others) ?? 0 } : { costUsd: null, ttftMs: null, latencyMs: null, outputTokens: null, others: 0 },49 }50 : null;51 return {52 version: 1,53 prompt: raw.prompt,54 systemPrompt: typeof raw.systemPrompt === "string" ? raw.systemPrompt : null,55 blind: raw.blind === true,56 attachmentCount: typeof raw.attachmentCount === "number" ? raw.attachmentCount : 0,57 parameters: isRecord(raw.parameters) ? raw.parameters : {},58 createdAt: typeof raw.createdAt === "string" ? raw.createdAt : new Date().toISOString(),59 models,60 responses,61 votes,62 winner,63 };64}6566function num(v: unknown): number | null {67 return typeof v === "number" && Number.isFinite(v) ? v : null;68}6970async function getShareSafe(id: string) {71 if (!id || id.length > 128 || !/^[\w-]+$/.test(id)) return null;72 try {73 const row = await getPublicArenaShare(id);74 if (!row) return null;75 const snapshot = parseSnapshot(row.snapshot);76 return snapshot ? { row, snapshot } : null;77 } catch {78 return null;79 }80}8182function title(s: ArenaShareSnapshot): string {83 const names = s.models.map((m) => m.displayName);84 return names.length <= 2 ? names.join(" vs ") : `${names.slice(0, 2).join(" vs ")} + ${names.length - 2} more`;85}8687export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {88 const { id } = await params;89 const share = await getShareSafe(id);90 return {91 title: share ? `${title(share.snapshot)} — Arena comparison` : "Shared Arena comparison",92 description: share ? share.snapshot.prompt.slice(0, 160) : "This shared comparison is unavailable.",93 robots: { index: false, follow: false, nocache: true },94 openGraph: share ? { title: `${title(share.snapshot)} — PolyLLM Arena`, description: share.snapshot.prompt.slice(0, 200), type: "article" } : undefined,95 };96}9798export default async function SharedArenaPage({ params }: { params: Promise<{ id: string }> }) {99 const { id } = await params;100 const share = await getShareSafe(id);101 if (!share) notFound();102 const s = share.snapshot;103 const created = new Date(s.createdAt);104 const params_ = Object.entries(s.parameters).filter(([, v]) => v !== undefined && v !== null);105106 return (107 <div className="flex min-h-dvh flex-col">108 <header className="glass sticky top-0 z-40 border-b border-border">109 <div className="mx-auto flex h-14 w-full max-w-6xl items-center justify-between gap-3 px-4 sm:px-6">110 <Link href="/" className="flex items-center gap-2.5 rounded-md" aria-label="PolyLLM home">111 <Logo size={22} />112 <span className="hidden text-[13px] text-fg-muted sm:inline">· Shared Arena comparison</span>113 </Link>114 <Button asChild size="sm">115 <Link href="/app/arena">116 Try the Arena117 <ArrowRight />118 </Link>119 </Button>120 </div>121 </header>122123 <main className="mx-auto w-full max-w-6xl flex-1 px-4 pb-16 pt-6 sm:px-6 sm:pt-10">124 <section className="border-b border-border pb-5">125 <p className="flex items-center gap-1.5 font-mono text-[11px] uppercase tracking-[0.14em] text-fg-subtle">126 <Swords className="size-3.5" /> Arena · {s.models.length} model{s.models.length === 1 ? "" : "s"}127 </p>128 <h1 className="mt-2 text-balance text-xl font-semibold leading-tight tracking-tight sm:text-2xl">{title(s)}</h1>129 <div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-2 text-[13px] text-fg-muted">130 <time dateTime={created.toISOString()}>{created.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}</time>131 <span aria-hidden>·</span>132 <ul className="flex flex-wrap gap-1.5" aria-label="Models compared">133 {s.models.map((m) => (134 <li key={m.key}>135 <Badge>136 <ProviderIcon provider={m.provider} size={12} />137 {m.displayName}138 </Badge>139 </li>140 ))}141 </ul>142 {s.blind ? <BlindNotice /> : null}143 </div>144 </section>145146 <section className="mt-6 space-y-3" aria-label="Prompt">147 <div className="flex justify-end">148 <div className="max-w-full rounded-2xl rounded-br-md bg-bg-muted px-4 py-2.5 text-[15px] leading-7 sm:max-w-[80%]">149 <p className="sr-only">Prompt:</p>150 <SimpleMarkdown>{s.prompt}</SimpleMarkdown>151 </div>152 </div>153 {s.systemPrompt || s.attachmentCount || params_.length ? (154 <details className="text-[12.5px] text-fg-muted">155 <summary className="cursor-pointer select-none">Settings</summary>156 <div className="mt-2 space-y-2 rounded-lg border border-border bg-bg-subtle/60 px-3 py-2">157 {s.systemPrompt ? (158 <p>159 <span className="font-medium text-fg">System prompt:</span> {s.systemPrompt}160 </p>161 ) : null}162 {s.attachmentCount ? (163 <p className="inline-flex items-center gap-1.5">164 <Paperclip className="size-3.5" /> {s.attachmentCount} attachment{s.attachmentCount === 1 ? "" : "s"} (not published)165 </p>166 ) : null}167 {params_.length ? (168 <ul className="flex flex-wrap gap-1.5">169 {params_.map(([k, v]) => (170 <li key={k} className="rounded-md border border-border bg-bg px-1.5 py-0.5 font-mono text-[11px]">171 {k}: {typeof v === "object" ? JSON.stringify(v) : String(v)}172 </li>173 ))}174 </ul>175 ) : null}176 </div>177 </details>178 ) : null}179 </section>180181 <section className="mt-6" aria-label="Responses">182 <SharedArenaView snapshot={s} />183 </section>184185 <aside className="mt-14 rounded-2xl border border-border bg-bg-elevated p-6 text-center shadow-sm sm:p-8">186 <p className="text-lg font-semibold tracking-tight">Compare models with your own keys</p>187 <p className="mx-auto mt-2 max-w-md text-[14px] leading-6 text-fg-muted">PolyLLM Arena runs one prompt through up to four models from OpenAI, Anthropic, Google, xAI, Mistral, DeepSeek, Kimi, OpenRouter and Cerebras — live, with real latency and cost.</p>188 <div className="mt-5 flex flex-col items-center justify-center gap-2 sm:flex-row">189 <Button asChild size="lg" className="w-full sm:w-auto">190 <Link href="/signup">191 Create a free account192 <ArrowRight />193 </Link>194 </Button>195 <Button asChild size="lg" variant="ghost" className="w-full sm:w-auto">196 <Link href="/">Learn more</Link>197 </Button>198 </div>199 </aside>200 </main>201202 <footer className="border-t border-border py-6 text-center text-xs text-fg-subtle">203 <p>204 Frozen snapshot shared by a PolyLLM user. Costs are estimates from list prices; model output can be wrong.{" "}205 <Link href="/privacy" className="underline-offset-4 hover:text-fg hover:underline">206 Privacy207 </Link>208 {" · "}209 <Link href="/terms" className="underline-offset-4 hover:text-fg hover:underline">210 Terms211 </Link>212 </p>213 </footer>214 </div>215 );216}217